﻿/*********************************************
 * Root math elements with event delegation.
 ********************************************/

function createRoot(jQ, root, textbox, editable) {
    var contents = jQ.contents().detach();

    if (!textbox) {
        jQ.addClass('mathquill-rendered-math');
    }

    root.jQ = jQ.attr(mqBlockId, root.id);
    root.revert = function () {
        jQ.empty().unbind('.mathquill')
          .removeClass('mathquill-rendered-math mathquill-editable mathquill-textbox')
          .append(contents);
    };

    var cursor = root.cursor = Cursor(root);

    root.renderLatex(contents.text());

    //textarea stuff
    var textareaSpan = root.textarea = $('<span class="textarea"><textarea></textarea></span>'),
      textarea = textareaSpan.children();

    /******
     * TODO [Han]: Document this
     */
    var textareaSelectionTimeout;
    root.selectionChanged = function () {
        if (textareaSelectionTimeout === undefined) {
            textareaSelectionTimeout = setTimeout(setTextareaSelection);
        }
        forceIERedraw(jQ[0]);
    };
    function setTextareaSelection() {
        textareaSelectionTimeout = undefined;
        var latex = cursor.selection ? '$' + cursor.selection.latex() + '$' : '';
        textareaManager.select(latex);
    }

    //prevent native selection except textarea
    jQ.bind('selectstart.mathquill', function (e) {
        if (e.target !== textarea[0]) e.preventDefault();
        e.stopPropagation();
    });

    //drag-to-select event handling
    var anticursor, blink = cursor.blink;
    jQ.bind('mousedown.mathquill', function (e) {
        textarea.focus(); //Fix for Mobile 
        function mousemove(e) {
            cursor.seek($(e.target), e.pageX, e.pageY);

            if (cursor[L] !== anticursor[L]
                || cursor.parent !== anticursor.parent) {
                cursor.selectFrom(anticursor);
            }

            return false;
        }

        // docmousemove is attached to the document, so that
        // selection still works when the mouse leaves the window.
        function docmousemove(e) {
            // [Han]: i delete the target because of the way seek works.
            // it will not move the mouse to the target, but will instead
            // just seek those X and Y coordinates.  If there is a target,
            // it will try to move the cursor to document, which will not work.
            // cursor.seek needs to be refactored.
            delete e.target;

            return mousemove(e);
        }

        function mouseup(e) {
            anticursor = undefined;
            cursor.blink = blink;
            if (!cursor.selection) {
                if (editable) {
                    cursor.show();
                }
                else {
                    textareaSpan.detach();
                }
            }

            // delete the mouse handlers now that we're not dragging anymore
            jQ.unbind('mousemove', mousemove);
            $(e.target.ownerDocument).unbind('mousemove', docmousemove).unbind('mouseup', mouseup);
        }

        setTimeout(function () { textarea.focus(); });
        // preventDefault won't prevent focus on mousedown in IE<9
        // that means immediately after this mousedown, whatever was
        // mousedown-ed will receive focus
        // http://bugs.jquery.com/ticket/10345

        cursor.blink = noop;
        cursor.seek($(e.target), e.pageX, e.pageY);

        anticursor = Point(cursor.parent, cursor[L], cursor[R]);

        if (!editable) jQ.prepend(textareaSpan);

        jQ.mousemove(mousemove);
        $(e.target.ownerDocument).mousemove(docmousemove).mouseup(mouseup);

        return false;
    });

    if (!editable) {
        var textareaManager = manageTextarea(textarea, { container: jQ });
        jQ.bind('cut paste', false).bind('copy', setTextareaSelection)
          .prepend('<span class="selectable">$' + root.latex() + '$</span>');
        textarea.blur(function () {
            cursor.clearSelection();
            setTimeout(detach); //detaching during blur explodes in WebKit
        });
        function detach() {
            textareaSpan.detach();
        }
        return;
    }

    var textareaManager = manageTextarea(textarea, {
        container: jQ,
        key: function (key, evt) {
            cursor.parent.bubble('onKey', key, evt);
        },
        text: function (text) {
            cursor.parent.bubble('onText', text);
        },
        cut: function (e) {
            if (cursor.selection) {
                setTimeout(function () {
                    cursor.prepareEdit();
                    cursor.parent.bubble('redraw');
                });
            }

            e.stopPropagation();
        },
        paste: function (text) {
            // FIXME HACK the parser in RootTextBlock needs to be moved to
            // Cursor::writeLatex or something so this'll work with
            // MathQuill textboxes
            if (text.slice(0, 1) === '$' && text.slice(-1) === '$') {
                text = text.slice(1, -1);
            }
            else {
                text = '\\text{' + text + '}';
            }

            cursor.writeLatex(text).show();
        }
    });

    jQ.prepend(textareaSpan);

    //root CSS classes
    jQ.addClass('mathquill-editable');
    if (textbox)
        jQ.addClass('mathquill-textbox');

    //focus and blur handling
    textarea.focus(function (e) {
        if (!cursor.parent)
            cursor.insAtRightEnd(root);
        cursor.parent.jQ.addClass('hasCursor');
        if (cursor.selection) {
            cursor.selection.jQ.removeClass('blur');
            setTimeout(root.selectionChanged); //re-select textarea contents after tabbing away and back
        }
        else
            cursor.show();
        e.stopPropagation();
    }).blur(function (e) {
        cursor.hide().parent.blur();
        if (cursor.selection)
            cursor.selection.jQ.addClass('blur');
        e.stopPropagation();
    });

    jQ.bind('focus.mathquill blur.mathquill', function (e) {
        textarea.trigger(e);
    }).blur();
}

var RootMathBlock = P(MathBlock, function (_, _super) {
    _.latex = function () {
        return _super.latex.call(this).replace(/(\\[a-z]+) (?![a-z])/ig, '$1');
    };
    _.text = function () {
        return this.foldChildren('', function (text, child) {
            return text + child.text();
        });
    };
    _.renderLatex = function (latex) {
        var jQ = this.jQ;

        jQ.children().slice(1).remove();
        this.ends[L] = this.ends[R] = 0;

        delete this.cursor.selection;
        this.cursor.insAtRightEnd(this).writeLatex(latex);
    };
    _.onKey = function (key, e) {
        switch (key) {
            case 'Ctrl-Shift-Backspace':
            case 'Ctrl-Backspace':
                while (this.cursor[L] || this.cursor.selection) {
                    this.cursor.backspace();
                }
                break;

            case 'Shift-Backspace':
            case 'Backspace':
                this.cursor.backspace();
                break;

                // Tab or Esc -> go one block right if it exists, else escape right.
            case 'Esc':
            case 'Tab':
            case 'Spacebar':
                var parent = this.cursor.parent;
                // cursor is in root editable, continue default
                if (parent === this.cursor.root) {
                    if (key === 'Spacebar') e.preventDefault();
                    return;
                }

                this.cursor.prepareMove();
                if (parent[R]) {
                    // go one block right
                    this.cursor.insAtLeftEnd(parent[R]);
                } else {
                    // get out of the block
                    this.cursor.insRightOf(parent.parent);
                }
                break;

                // Shift-Tab -> go one block left if it exists, else escape left.
            case 'Shift-Tab':
            case 'Shift-Esc':
            case 'Shift-Spacebar':
                var parent = this.cursor.parent;
                //cursor is in root editable, continue default
                if (parent === this.cursor.root) {
                    if (key === 'Shift-Spacebar') e.preventDefault();
                    return;
                }

                this.cursor.prepareMove();
                if (parent[L]) {
                    // go one block left
                    this.cursor.insAtRightEnd(parent[L]);
                } else {
                    //get out of the block
                    this.cursor.insLeftOf(parent.parent);
                }
                break;

                // Prevent newlines from showing up
            case 'Enter': break;


                // End -> move to the end of the current block.
            case 'End':
                this.cursor.prepareMove().insAtRightEnd(this.cursor.parent);
                break;

                // Ctrl-End -> move all the way to the end of the root block.
            case 'Ctrl-End':
                this.cursor.prepareMove().insAtRightEnd(this);
                break;

                // Shift-End -> select to the end of the current block.
            case 'Shift-End':
                while (this.cursor[R]) {
                    this.cursor.selectRight();
                }
                break;

                // Ctrl-Shift-End -> select to the end of the root block.
            case 'Ctrl-Shift-End':
                while (this.cursor[R] || this.cursor.parent !== this) {
                    this.cursor.selectRight();
                }
                break;

                // Home -> move to the start of the root block or the current block.
            case 'Home':
                this.cursor.prepareMove().insAtLeftEnd(this.cursor.parent);
                break;

                // Ctrl-Home -> move to the start of the current block.
            case 'Ctrl-Home':
                this.cursor.prepareMove().insAtLeftEnd(this);
                break;

                // Shift-Home -> select to the start of the current block.
            case 'Shift-Home':
                while (this.cursor[L]) {
                    this.cursor.selectLeft();
                }
                break;

                // Ctrl-Shift-Home -> move to the start of the root block.
            case 'Ctrl-Shift-Home':
                while (this.cursor[L] || this.cursor.parent !== this) {
                    this.cursor.selectLeft();
                }
                break;

            case 'Left': this.cursor.moveLeft(); break;
            case 'Shift-Left': this.cursor.selectLeft(); break;
            case 'Ctrl-Left': break;

            case 'Right': this.cursor.moveRight(); break;
            case 'Shift-Right': this.cursor.selectRight(); break;
            case 'Ctrl-Right': break;

            case 'Up': this.cursor.moveUp(); break;
            case 'Down': this.cursor.moveDown(); break;

            case 'Shift-Up':
                if (this.cursor[L]) {
                    while (this.cursor[L]) this.cursor.selectLeft();
                } else {
                    this.cursor.selectLeft();
                }

            case 'Shift-Down':
                if (this.cursor[R]) {
                    while (this.cursor[R]) this.cursor.selectRight();
                }
                else {
                    this.cursor.selectRight();
                }

            case 'Ctrl-Up': break;
            case 'Ctrl-Down': break;

            case 'Ctrl-Shift-Del':
            case 'Ctrl-Del':
                while (this.cursor[R] || this.cursor.selection) {
                    this.cursor.deleteForward();
                }
                break;

            case 'Shift-Del':
            case 'Del':
                this.cursor.deleteForward();
                break;

            case 'Meta-A':
            case 'Ctrl-A':
                //so not stopPropagation'd at RootMathCommand
                if (this !== this.cursor.root) return;

                this.cursor.prepareMove().insAtRightEnd(this);
                while (this.cursor[L]) this.cursor.selectLeft();
                break;

            default:
                return false;
        }
        e.preventDefault();
        return false;
    };
    _.onText = function (ch) {
        this.cursor.write(ch);
        return false;
    };
});

var RootMathCommand = P(MathCommand, function (_, _super) {
    _.init = function (cursor) {
        _super.init.call(this, '$');
        this.cursor = cursor;
    };
    _.htmlTemplate = '<span class="mathquill-rendered-math">&0</span>';
    _.createBlocks = function () {
        this.ends[L] =
        this.ends[R] =
          RootMathBlock();

        this.blocks = [this.ends[L]];

        this.ends[L].parent = this;

        this.ends[L].cursor = this.cursor;
        this.ends[L].write = function (cursor, ch, replacedFragment) {
            if (ch !== '$')
                MathBlock.prototype.write.call(this, cursor, ch, replacedFragment);
            else if (this.isEmpty()) {
                cursor.insRightOf(this.parent).backspace().show();
                VanillaSymbol('\\$', '$').createLeftOf(cursor);
            }
            else if (!cursor[R])
                cursor.insRightOf(this.parent);
            else if (!cursor[L])
                cursor.insLeftOf(this.parent);
            else
                MathBlock.prototype.write.call(this, cursor, ch, replacedFragment);
        };
    };
    _.latex = function () {
        return '$' + this.ends[L].latex() + '$';
    };
});

var RootTextBlock = P(MathBlock, function (_) {
    _.renderLatex = function (latex) {
        var self = this;
        var cursor = self.cursor;
        self.jQ.children().slice(1).remove();
        self.ends[L] = self.ends[R] = 0;
        delete cursor.selection;
        cursor.show().insAtRightEnd(self);

        var regex = Parser.regex;
        var string = Parser.string;
        var eof = Parser.eof;
        var all = Parser.all;

        // Parser RootMathCommand
        var mathMode = string('$').then(latexMathParser)
          // because TeX is insane, math mode doesn't necessarily
          // have to end.  So we allow for the case that math mode
          // continues to the end of the stream.
          .skip(string('$').or(eof))
          .map(function (block) {
              // HACK FIXME: this shouldn't have to have access to cursor
              var rootMathCommand = RootMathCommand(cursor);

              rootMathCommand.createBlocks();
              var rootMathBlock = rootMathCommand.ends[L];
              block.children().adopt(rootMathBlock, 0, 0);

              return rootMathCommand;
          })
        ;

        var escapedDollar = string('\\$').result('$');
        var textChar = escapedDollar.or(regex(/^[^$]/)).map(VanillaSymbol);
        var latexText = mathMode.or(textChar).many();
        var commands = latexText.skip(eof).or(all.result(false)).parse(latex);

        if (commands) {
            for (var i = 0; i < commands.length; i += 1) {
                commands[i].adopt(self, self.ends[R], 0);
            }

            var html = self.join('html');
            MathElement.jQize(html).appendTo(self.jQ);

            this.finalizeInsert();
        }
    };
    _.onKey = function (key) {
        if (key === 'Spacebar' || key === 'Shift-Spacebar') return;
        RootMathBlock.prototype.onKey.apply(this, arguments);
    };
    _.onText = RootMathBlock.prototype.onText;
    _.write = function (cursor, ch, replacedFragment) {
        if (replacedFragment) replacedFragment.remove();
        if (ch === '$')
            RootMathCommand(cursor).createLeftOf(cursor);
        else {
            var html;
            if (ch === '<') html = '&lt;';
            else if (ch === '>') html = '&gt;';
            VanillaSymbol(ch, html).createLeftOf(cursor);
        }
    };
});